Análise de dados NYC Taxi Trips

Autor: Lauro César de Oliveira Teixeira

Data: Belo Horizonte - 12 de Agosto de 2019

Versão: 1.0

Contato: Github - LinkedIn - Gmail

Introdução

Este trabalho tem como o objetivo fazer uma análise em cima de uma base dados sobre as viagens de táxi em Nova York no ano de 2009 a 2012. Todos os dados usados foram disponibilizados pela Data Sprints e estão sendo armazenados em buckets da Amazon para uma melhor disponibilidade, mas originalmente são derivados de fontes de dados abertas. Bases semelhantes podem ser encontradas no Keggle ou em sites como NYC.gov, um site governamental que disponibiliza algumas DGA’s (Dados Governamentais Abertas).

O intuito dessa análise é mostrar de forma bem simples a manipulação de bases de dados muito grandes, utilizando serviços da amazon para melhor processamento e linguagens como SQL e Python para consultar, manipular e criar visualizações gráficas de forma eficiente e simples.

As tecnologias usadas para a composição desta análise são:

E as bibliotecas utilizadas foram:

Ao decorrer da análise, todas as bibliotecas utilizadas serão explicadas com mais detalhes para que o leitor não sinta-se prejudicado por não entender o porque e como está sendo a implementação de cada parte do código.

Preparando os dados

O primeiro dataset a ser estudado, são os dados sobre as viagens de táxi em Nova York. É uma coleção com 4 arquivos, possuindo 500MB aproximadamente cada, ou seja, 2GB juntando todos os dados que se equivale a 4 milhões de registros.

Para não ocupar muito armazenamento em disco, e não pesar para fazer tráfego de rede, foi usado a estratégia de se criar um cluster simples na Amazon contendo o serviço de Redshift DatawareHouse para um melhor desempenho em consultas paralelizadas e uma instância EC2 (Elastic Compute Cloud) a qual vai rodar um script em python que irá coletar todos os dados dos 4 datasets e o fará todo o ETL para inseri-los no banco de dados da Redshift.

  • O cluster da Amazon RedShift foi criado com as configurações mais simples, é um banco dedos baseado em PostgresSQL que tem um desempenho grande para consultas paralela obtendo assim uma rápida resposta por execução de consulta.É composto por um cluster com 2 nós, sendo que possuí a arquitetura mais simples permitida, instâncias DC2 large com 15.25GiB de RAM e um armazenamento SSD 160GB.
  • A instância EC2 utilizada foi uma T2 Large, escolhida por ser dentre as máquinas mais simple que contem a maior quantidade de CPU's e uma maior carga de rede. Está rodando Ubuntu server 18.04 LTS como sistema operacional, possui 8GB de memória em disco, 2 CPU's e 16GB de RAM. A máquina pode ser um pouco a mais do que o necessário, mas será desativada logo após o ETL, que não irá demorar nem 30 minutos.

O script em sql utilizado de construção da tabela Trip que representa as viagens do dataset no banco de dados:

DROP SCHEMA IF EXISTS NYC CASCADE;
CREATE SCHEMA IF NOT EXISTS NYC;

CREATE TABLE IF NOT EXISTS NYC.TRIP(
    VENDOR_ID VARCHAR(50) NOT NULL,
    PICKUP_DATETIME TIMESTAMP NOT NULL,
    DROPOFF_DATETIME TIMESTAMP NOT NULL,
    PASSENGER_COUNT SMALLINT  NOT NULL,
    TRIP_DISTANCE NUMERIC(7,2) NOT NULL,
    PICKUP_LONGITUDE NUMERIC(11,7),
    PICKUP_LATITUDE NUMERIC(11,7),
    DROPOFF_LONGITUDE NUMERIC(11,7),
    DROPOFF_LATITUDE NUMERIC(11,7),
    RATE_CODE VARCHAR(100) NULL,
    STORE_AND_FWD_FLAG VARCHAR(100) NULL,
    PAYMENT_TYPE VARCHAR(50) NOT NULL,
    FARE_AMOUNT DECIMAL(6,2) NOT NULL DEFAULT 0,
    SURCHARGE DECIMAL(6,2) NOT NULL DEFAULT 0,
    TIP_AMOUNT DECIMAL(6,2) NOT NULL DEFAULT 0,
    TOTAL_AMOUNT DECIMAL(6,2) NOT NULL DEFAULT 0,
    TOLLS_AMOUNT DECIMAL(6,2) NOT NULL DEFAULT 0
);

Descrição das colunas:

  • vendor_id: Uma string que representa o fornecedor da viagem.
  • pickup_datetime: O horário em que o passageiro iniciou a viagem.
  • dropoff_datetime: O horário em que o passageiro finalizou sua viagem.
  • passanger_count: Quantidade de passageiros na viagem.
  • trip_distance: A distância arredondada em milhas registrada pelo taxímetro
  • pickup_longitude: As cordenadas de longitude de onde o(s) passageiro(s) iniciaram a corrida.
  • pickup_latitude: As cordenadas de latitude de onde o(s) passageiro(s) iniciaram a corrida.
  • dropoff_longitude: As cordenadas de longitude de onde o(s) passageiro(s) finalizaram a corrida.
  • dropoff_latitude: As cordenadas de latitude de onde o(s) passageiro(s) finalizaram a corrida.
  • rate_code: Uma taxa que representa a taxa em vigor no final da viagem (quase nunca preenchida no dataset).
  • store_and_fwd_flag: Esse campo identifica se os dados da viagem foram armazenados na memória do veículo antes de ser retornada para seu fornecedor por motivos de falta de conexão. Sendo 'Y' para sim e 'N' para não.
  • payment_type: A forma de pagamento que foi utilizada na viagem. Pode variar entre 'Credit card' - Cartão de crédito, 'Cash' - dinheiro, 'No charge' - Sem custo, 'Dispute' - disputa, 'Unknow' - desconhecida, 'Voided trip' - Viagem anulada.
  • fare_amount: Tarifa calculada pelo taxímetro por metro, tomando como base tempo e distância.
  • surcharge: Tarifa extra cobrada em casos como viagens em horários de pico ou viagens em horários noturnos.
  • tip_amount: Gorjeta dada ao motorista, preenchida automaticamente apenas em casos de cartão de credito.
  • total_amount: Quantidade total paga pelo(s) passageiro(s). Gorjetas em dinheiros não ficam inclusas nesse campo.
  • tolls_amount: Quantidade gasta em pedágios.

Apos a criação da tabela de viagens(Trip), foram criado os acessos de usuários para fazer o processamento. (OBS: as senhas reais foram substituidas por XXXXXXXX apenas por procedimentos de segurança.)

CREATE USER populator WITH PASSWORD 'XXXXXXXXX';
CREATE USER analytics WITH PASSWORD 'XXXXXXXXX';

GRANT USAGE ON SCHEMA NYC TO populator;
REVOKE ALL ON nyc.trip from populator CASCADE;
GRANT SELECT, INSERT, DELETE, UPDATE ON NYC.trip TO populator;

GRANT USAGE ON SCHEMA NYC TO analytics;
REVOKE ALL ON nyc.trip from analytics CASCADE;
GRANT SELECT ON  NYC.trip TO analytics;

No script acima, foram criados 2 usuários, o populator que tera as permissões necessárias para popular a tabela e o analytics que será o responsável por executar consultas.

A seguir está o código do ETL dos dados do dataset:

# -*- coding: utf-8 -*-
"""
Created on Sat Sep  7 10:57:45 2019

@author: Lauro Oliveira <0lilauro7@gmail.com>
"""

import psycopg2
from psycopg2.extras import execute_values 
import json 
import requests
from pprint import pprint
from multiprocessing import Process
from datetime import datetime

URLS = [
    'https://s3.amazonaws.com/data-sprints-eng-test/data-sample_data-nyctaxi-trips-2009-json_corrigido.json',
     'https://s3.amazonaws.com/data-sprints-eng-test/data-sample_data-nyctaxi-trips-2010-json_corrigido.json',
     'https://s3.amazonaws.com/data-sprints-eng-test/data-sample_data-nyctaxi-trips-2011-json_corrigido.json',
     'https://s3.amazonaws.com/data-sprints-eng-test/data-sample_data-nyctaxi-trips-2012-json_corrigido.json'
]

def timerize(f):
    def remake_func(*args, **kwargs):
        start_time = datetime.now()
        f(*args, **kwargs)
        delta = datetime.now() - start_time
        pprint("Finish function {}. - time ellipsed: {}".format(f.__name__, delta))
    return remake_func


def get_connection():
    DB_HOST = 'xxxxxxx.cyy2ldnhnbji.xxxxxxxx.redshift.amazonaws.com'
    DB_USERNAME = 'populator'
    DB_PORT = '5439'
    DB_DATABASE = 'analyze'
    DB_PASSWORD = 'XXXXXXXXXXXXXX'

    try:
        connection = psycopg2.connect(
            host = DB_HOST,
            user = DB_USERNAME,
            password = DB_PASSWORD,
            port = int(DB_PORT),
            dbname = DB_DATABASE
        )
        return connection 

    except Exception as exception_connection: 
        print(str(exception_connection))
        exit("Was impossible to connect on {} database".format(DB_DATABASE))
        return None

def insert(values, con): 
    command = """
        INSERT 
        INTO NYC.TRIP (
            DROPOFF_DATETIME,
            DROPOFF_LATITUDE,
            DROPOFF_LONGITUDE,
            FARE_AMOUNT,
            PASSENGER_COUNT,
            PAYMENT_TYPE,
            PICKUP_DATETIME,
            PICKUP_LATITUDE,
            PICKUP_LONGITUDE,
            RATE_CODE,
            STORE_AND_FWD_FLAG,
            SURCHARGE,
            TIP_AMOUNT,
            TOLLS_AMOUNT,
            TOTAL_AMOUNT,
            TRIP_DISTANCE,
            VENDOR_ID
        ) VALUES %s """

    try:
        execute_values(con.cursor(), command, values)
    except:
        print(" ======= An erro ocurred !!")
    finally:
        con.commit()

def process_lines(url):
    con = get_connection()
    with requests.get(url, stream=True) as request:
        request.raise_for_status()
        i = 0
        counter = 0
        insertion_values = []
        for chunk in request.iter_lines(decode_unicode=True):
            if chunk:
                try:
                    chunk_dict = json.loads(chunk)
                    insertion_values.append(
                        (
                            chunk_dict['dropoff_datetime'],
                            chunk_dict['dropoff_latitude'],
                            chunk_dict['dropoff_longitude'],
                            chunk_dict['fare_amount'],
                            chunk_dict['passenger_count'],
                            chunk_dict['payment_type'],
                            chunk_dict['pickup_datetime'],
                            chunk_dict['pickup_latitude'],
                            chunk_dict['pickup_longitude'],
                            chunk_dict['rate_code'],
                            chunk_dict['store_and_fwd_flag'],
                            chunk_dict['surcharge'],
                            chunk_dict['tip_amount'],
                            chunk_dict['tolls_amount'],
                            chunk_dict['total_amount'],
                            chunk_dict['trip_distance'],
                            chunk_dict['vendor_id'],    
                        )
                    )
                    counter+= 1
                    i+= 1
                except Exception as ex : 
                    print(ex)

            pprint(i)

            if counter >= 4000:
                insert(insertion_values, con)
                counter = 0
                insertion_values = []

@timerize
def pool_function(urls): 
    for url in urls:
        proc = Process(target=process_lines, args=(url,))
        proc.start()
    proc.join()

if __name__ == '__main__':
    pool_function(URLS)

Os datasets de viagens foram disponibilizados pela Data Sprints, que publicou em 4 links diferentes cada um dos arquivos, sendo referente aos anos de coleta de dados: 2009, 2010, 2011, 2012. Os dados estão estruturados como objetos, sendo cada linha, um dicionario de chave e valor referente a viagem.

Para a coleta desses dados foi usado uma função da biblioteca requests do pyhton que consegue fazem um stream dos arquivos de resposta das requisições enviadas. E com o auxilio da função iter_lines() foi possivel iterar cada registro do stream do arquivo em download para lê-lo como um json e em seguida carrega-lo no banco de dados. Tudo isso pode ser visto na função process_lines().

Como todos os dados, estão dividido em 4 endereços, foi utilizado a biblioteca Multiprocessing que nos disponibiliza uma variedade de funções e classes para executar de forma paralela varios processos. Como pode notar na função pool_function(), foi feito uma iteração para executar todos os 4 endereço de forma assincrona e paralela.

A inserção no banco de dados acontece a cada 4000 registros processados por meio da função execute_values() da biblioteca psycopg2, mas em caso de algum gargalo, é possível optimizar o processo diminuindo a quantidade de inserção por vez, como por exemplo reduzir o valor para 2000 ou em casos criticos 800.

Como vantagem de tudo, ainda temos o fato de que tudo foi executado sem o uso dos 4GB requeridos de memória em disco.

Esse processo foi feito utilzando uma máquina secundária pois não é possíovel fazer uso de bibliotecas como o Multiprocessing em notebooks hosteados em windows, pois eles fazem fork do processo que executa o notebook e o windows restringe isso.

Instalação das Bibliotecas

Para o começo das análises, vamos importar algumas bibliotecas que serão muito utilizadas durante todo o estudo.

Neste momento vamos importar a biblioteca Numpy, que é um pacote para computação científica que contém como principal módulo, arrays multidimensionais que pode se trabalhar algebra linear de forma muito simples. Além de ser requerimento de outras bibliotecas como o Pandas.

O Pandas é uma biblioteca de estrutura de dados que serve para alta performance de análise de dados.

Altair e Matplotlib são 2 bibliotecas excelentes para a visualização de dados em gráficos diversificados. Ambas trabalham com estruturas de dados como o Pandas. O Altair tem como peculiaridade o uso do módulo Vega para a renderização de gráficos em notebooks como o utilizado para a apresentação do estudo. E foi escolhida como um adicional para o Matplotlib por conseguir processar gráficos encima de mapas de forma muito simples.

Usaremos o Psycopg2 para estabelecer conexões com a base de dados da Amazon Redshift para fazermos pesquisas usando queries em SQL.

Nesse momento vamos fazer a instalação das bibliotecas que vamos utilizar a seguir em nosso processo.

In [1]:
! pip install psycopg2 numpy pandas scikit-learn scipy matplotlib altair vega_datasets vega IPython boto3
Requirement already satisfied: psycopg2 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (2.7.6.1)
Requirement already satisfied: numpy in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (1.17.0)
Requirement already satisfied: pandas in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (0.25.0)
Requirement already satisfied: scikit-learn in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (0.21.3)
Requirement already satisfied: scipy in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (1.3.0)
Requirement already satisfied: matplotlib in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (3.1.1)
Requirement already satisfied: altair in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (3.2.0)
Requirement already satisfied: vega_datasets in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (0.7.0)
Requirement already satisfied: vega in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (2.5.0)
Requirement already satisfied: IPython in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (7.7.0)
Requirement already satisfied: boto3 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (1.9.222)
Requirement already satisfied: pytz>=2017.2 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from pandas) (2019.1)
Requirement already satisfied: python-dateutil>=2.6.1 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from pandas) (2.8.0)
Requirement already satisfied: joblib>=0.11 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from scikit-learn) (0.13.2)
Requirement already satisfied: cycler>=0.10 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from matplotlib) (0.10.0)
Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from matplotlib) (2.4.0)
Requirement already satisfied: kiwisolver>=1.0.1 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from matplotlib) (1.1.0)
Requirement already satisfied: jinja2 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from altair) (2.10.1)
Requirement already satisfied: entrypoints in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from altair) (0.3)
Requirement already satisfied: toolz in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from altair) (0.10.0)
Requirement already satisfied: six in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from altair) (1.12.0)
Requirement already satisfied: jsonschema in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from altair) (3.0.1)
Requirement already satisfied: traitlets>=4.2 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from IPython) (4.3.2)
Requirement already satisfied: jedi>=0.10 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from IPython) (0.13.3)
Requirement already satisfied: pickleshare in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from IPython) (0.7.5)
Requirement already satisfied: prompt-toolkit<2.1.0,>=2.0.0 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from IPython) (2.0.9)
Requirement already satisfied: colorama; sys_platform == "win32" in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from IPython) (0.4.1)
Requirement already satisfied: decorator in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from IPython) (4.4.0)
Requirement already satisfied: pygments in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from IPython) (2.4.2)
Requirement already satisfied: setuptools>=18.5 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from IPython) (41.0.1)
Requirement already satisfied: backcall in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from IPython) (0.1.0)
Requirement already satisfied: botocore<1.13.0,>=1.12.222 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from boto3) (1.12.222)
Requirement already satisfied: s3transfer<0.3.0,>=0.2.0 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from boto3) (0.2.1)
Requirement already satisfied: jmespath<1.0.0,>=0.7.1 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from boto3) (0.9.4)
Requirement already satisfied: MarkupSafe>=0.23 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from jinja2->altair) (1.1.1)
Requirement already satisfied: attrs>=17.4.0 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from jsonschema->altair) (19.1.0)
Requirement already satisfied: pyrsistent>=0.14.0 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from jsonschema->altair) (0.14.11)
Requirement already satisfied: ipython_genutils in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from traitlets>=4.2->IPython) (0.2.0)
Requirement already satisfied: parso>=0.3.0 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from jedi>=0.10->IPython) (0.5.0)
Requirement already satisfied: wcwidth in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from prompt-toolkit<2.1.0,>=2.0.0->IPython) (0.1.7)
Requirement already satisfied: urllib3<1.26,>=1.20; python_version >= "3.4" in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from botocore<1.13.0,>=1.12.222->boto3) (1.24.2)
Requirement already satisfied: docutils<0.16,>=0.10 in c:\users\lauro.teixeira\appdata\local\continuum\anaconda3\envs\py36\lib\site-packages (from botocore<1.13.0,>=1.12.222->boto3) (0.15.1)
In [2]:
import psycopg2
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import altair as alt

Analise das Viagens

Vamos fazer alguns estudos e ver um pouco da nossa base de dados.

Para começar, vamos fazer uma consulta simples que retorna-rá a distância percorrida em viagens com no máximo 2 passageiros.

Na função a seguir vamos criar a função de conexão com o banco de dados, que ira nos ajudar sempre que precisarmos fazer uma nova consulta. Nela, utilizamos a biblioteca do psycopg que consegue conectar com o Redshift da Amazon de forma bem simples.

In [3]:
def get_connection():
    DB_HOST = 'redshift-cluster-nyc.cyy2ldnhnbji.us-east-2.redshift.amazonaws.com'
    DB_USERNAME = 'analytics'
    DB_PORT = '5439'
    DB_DATABASE = 'analyze'
    DB_PASSWORD = 'wVRF2iWdqoEHX3EeFLwugh'

    try:
        connection = psycopg2.connect(
            host = DB_HOST,
            user = DB_USERNAME,
            password = DB_PASSWORD,
            port = int(DB_PORT),
            dbname = DB_DATABASE
        )
        return connection 

    except Exception as exception_connection: 
        print(str(exception_connection))
        exit("Was impossible to connect on {} database".format(DB_DATABASE))
        return None

Após criarmos a conexão com o banco, vamos agora focar em como adequar nossos dados. Para isso, utilizaremos a biblioteca do pandas. Por meio de uma de suas funções, a read_sql() vamos conseguir ja trazer a query consultada no banco de dados para uma estrutura pandas.

In [4]:
query = """
SELECT
    T.TRIP_DISTANCE as "distance"
FROM NYC.TRIP T
WHERE 
    T.PASSENGER_COUNT <= 2;
"""

base_dataset = pd.read_sql(query, con=get_connection())
base_dataset.sample(10)
Out[4]:
distance
82353 1.15
1322257 3.00
1626636 1.60
1747046 4.30
1240213 3.03
2272396 2.78
69938 1.92
987766 3.00
3258715 2.23
2874275 0.90
In [5]:
fig, ax = plt.subplots()
fig.figsize=(12, 10)
ax.figure.set_size_inches(12, 10)
ax.set_ylabel("Frequência de ocorrência", fontsize=13)
ax.set_xlabel("Milhas percorridas", fontsize=13)
ax.yaxis.set_tick_params(labelsize=13)
ax.xaxis.set_tick_params(labelsize=13)
plt.hist(base_dataset.distance, 10)
plt.show()

Com o gráfico acima, conseguimos notar que a média está obviamente localizada entre distâncias menores que 10 milhas. Para verificar isso melhor, vamos ampliar a visão dos dados e plotar uma amostra que contenha apenas os registros menores que 5 milhas.

In [6]:
sample = base_dataset.loc[base_dataset.distance <= 5]
sample.sample(3)
Out[6]:
distance
1332921 0.80
2596418 0.77
1565751 1.10
In [7]:
edge = .5
bins = np.arange(edge, 5,edge)
y_values = np.array(())

for value in bins:
    counter = base_dataset.loc[(base_dataset.distance > value - edge) & (base_dataset.distance <= value)].count()
    y_values = np.append(y_values, counter)
y_values
Out[7]:
array([195248., 680016., 620032., 447648., 312220., 219916., 158024.,
       115304.,  85536.])
In [8]:
fig, ax = plt.subplots()
fig.figsize=(12, 10)
ax.figure.set_size_inches(12, 10)
ax.set_ylabel("Frequência de ocorrência", fontsize=13)
ax.set_xlabel("Milhas percorridas", fontsize=13)
ax.yaxis.set_tick_params(labelsize=13)
ax.xaxis.set_tick_params(labelsize=13)
ax.bar(bins, y_values, color="C0", align = 'center', width=.4)
plt.show()

Com isso vemos que a distribuição da distancia das viagens com 2 passageiros estão distribuídas em maior em pequenas corridas com menos de 2 milhas. E a média das distancias é:

In [9]:
base_dataset.distance.mean()
Out[9]:
2.662527023916964

Agora vamos ver outras informações de nossa base

Para nossas proximas atividades, vamos usar a seguinte premissa: "Quais os 3 maiores companhias em quantidade total de dinheiro arrecadado"

Para as próximas análises vamos utilizar um dataset extra, ele é um csv pequeno que contem informações sobre todos os fornecedores. Possuí apenas 4 linhas de registro, então vamos importa-ló de forma padrão.

Os dados present no CSV de fornecedores são:

  • vendor_id: Representa a chave da companhia de táxi.
  • name: Representa o nome completo da companhia.
  • address: É o endereço da companhia.
  • city: Cidade do endereço da companhia.
  • state: O estado em que a companhia se encontra.
  • country: País em que a companhia se encontra.
  • contact: Email de contato da companhia em questão.
  • current: Status da companhia atualmente, sendo 'Yes' para ativa e 'No' para desativada.
In [10]:
vendors = pd.read_csv('./dataset/data-vendor_lookup-csv.csv', sep=",")
vendors
Out[10]:
vendor_id name address city state zip country contact current
0 CMT Creative Mobile Technologies, LLC 950 4th Road Suite 78 Brooklyn NY 11210 USA contactCMT@gmail.com Yes
1 VTS VeriFone Inc 26 Summit St. Flushing NY 11354 USA admin@vtstaxi.com Yes
2 DDS Dependable Driver Service, Inc 8554 North Homestead St. Bronx NY 10472 USA 9778896500 Yes
3 TS Total Solutions Co Five Boroughs Taxi Co. Brooklyn NY 11229 USA mgmt@5btc.com Yes
4 MT Mega Taxi 4 East Jennings St. Brooklyn NY 11228 USA contact@megataxico.com No

Vamos agora consultar nossa base de dados utilizando uma query que resultara o resultado das 3 maiores companhias em questão de dinheiro arrecadado.

(Obs: Para fazer a consulta, apenas o campo de preço total da corrida está sendo considerado. Não será utilizado nenhuma forma de "limpar" o valor, como retirar a gorjeta ou mesmo ignorar as viagens classifcadas como anuladas.)

In [11]:
query = """
SELECT
    SUM(T.TOTAL_AMOUNT) AS "value_accumulated",
    T.VENDOR_ID AS "vendor"
FROM NYC.TRIP T
GROUP BY T.VENDOR_ID 
ORDER BY SUM(T.TOTAL_AMOUNT) DESC
LIMIT 3;

"""

base_dataset = pd.read_sql(query, con=get_connection())
base_dataset
Out[11]:
value_accumulated vendor
0 19549084.28 CMT
1 19043434.00 VTS
2 2714901.72 DDS
In [12]:
 base_dataset.vendor.count() +1
for idx, register in enumerate(range(1, base_dataset.vendor.count() + 1)):
    vendor = base_dataset.loc[idx, 'vendor']
    print("{}º lugar, com US$ {} - {}".format(
        idx + 1,
        base_dataset.loc[idx, 'value_accumulated'],
        vendors.loc[vendors.vendor_id == vendor].name.values[0]
    ))
1º lugar, com US$ 19549084.28 - Creative Mobile Technologies, LLC
2º lugar, com US$ 19043434.0 - VeriFone Inc
3º lugar, com US$ 2714901.72 - Dependable Driver Service, Inc

Mudando o foco

A seguir vamos então fazer uma análise do histograma de todos os meses por ano das corridas pagas em dinheiro.

O campo payment_type no banco de dados não está recebendo os valores adequados, existem grandes variações dos valores existentes lá. E como queremos fazer uma consulta por método de pagamento em dinheiro(Cash), é um pouco difícil ajustar a query para trazer todos esse valores.

Para essa ocasião também existe um dataset pequeno em CSV que possuí todas as variações das palavras referentes aos métodos de pagamento, incluiindo o "Cash"

Vamos pegar esse dataset e montar uma string contendo as variações da palavra "Cash" no banco dedos que será usada na consulta.

In [13]:
payments = pd.read_csv('./dataset/data-payment_lookup-csv.csv', sep=',')
payments_cash = payments.loc[payments.B =='Cash'].A.values.tolist()
mapped = map(
    lambda x: "'{}'".format(x),
    payments_cash
)
cash_words = " ,".join(list(mapped))
cash_words
Out[13]:
"'Cas' ,'CAS' ,'Cash' ,'CASH' ,'CSH'"

Agora vamos usar os valores que geramos para consultar na base de dados.

In [14]:
query = """
SELECT
    COUNT(T.VENDOR_ID) AS "occurency",
    CONCAT(
        CONCAT(
            CAST(DATE_PART('YEAR', T.pickup_datetime) AS VARCHAR(4)),
            '/'::VARCHAR(1)
        ), 
        CAST(DATE_PART('MONTH', T.pickup_datetime) AS VARCHAR(2))
    ) AS "key",
    CAST(
        DATE_PART('YEAR', T.PICKUP_DATETIME) AS VARCHAR(4)
    ) AS "year",
    CAST(
        DATE_PART('MONTH', T.PICKUP_DATETIME) AS VARCHAR(2)
    ) AS "month"
FROM NYC.TRIP T
WHERE T.payment_type IN ({})
GROUP BY 3, 4;
""".format(cash_words)

base_dataset = pd.read_sql(query, con=get_connection())
base_dataset.sample(3)
Out[14]:
occurency key year month
36 70876 2010/11 2010 11
31 83935 2012/5 2012 5
21 80926 2012/1 2012 1
In [15]:
base_dataset.month = base_dataset.month.apply(pd.to_numeric, errors='ignore')
base_dataset.year = base_dataset.year.apply(pd.to_numeric, errors='ignore')
base_dataset = base_dataset.sort_values(['year', 'month'], ascending=[True, True])
base_dataset.sample(3)
Out[15]:
occurency key year month
13 66824 2009/1 2009 1
38 73487 2010/7 2010 7
37 66837 2010/2 2010 2

Como é possivel perceber, está faltando 4 meses no histórico. Poderiamos adicionar as linhas faltantes com o valor de 0 ou com a média do mês anterior e o sucessor, mas por hora vamos ignorar esse problema.

In [16]:
fig, ax = plt.subplots()
fig.figsize=(20, 10)
ax.figure.set_size_inches(20, 10)
ax.set_ylabel("Corridas acontecidas", fontsize=14)
ax.set_xlabel("Ano/mês", fontsize=14)
ax.yaxis.set_tick_params(labelsize=12)
ax.xaxis.set_tick_params(labelsize=13)

ax.bar(base_dataset.key, base_dataset.occurency, color="C0", align = 'center', width=.4)
ax.set_xticklabels(base_dataset.key, rotation=90, fontsize=13)
plt.show()

A partir dessa visualização se torna notável que nos finais de ano, a quantidade de corridas em dinheiro sempre acontece uma queda, e que também, o mesmo padrão de ocorrências de corridas se mantem o mesmo para os repetidos anos.

Novas Métricas

Agora que já vimos o total de custo das corridas de táxi por mês, vamos focar especificamente nos 3 ultimos meses do ultimo ano de registro. E vamos veficar o quanto de gorjeta as corridas de táxis renderam para cada dia.

Vamos construir uma query que fará toda a seleção dos valores, em seguida vamos mostrar em uma série temporal esses valores.

In [17]:
query = """
SELECT
    SUM(T.TIP_AMOUNT) AS "tip",
    CONCAT(
        CONCAT(
            CAST(DATE_PART('MONTH', T.PICKUP_DATETIME) AS VARCHAR(2)), 
            '/'
        ),
        CAST(DATE_PART('DAY', T.PICKUP_DATETIME) AS VARCHAR(2))
    ) AS "key",
    CAST(
        DATE_PART('MONTH', T.PICKUP_DATETIME) AS VARCHAR(2)
    ) AS "month",
    CAST(
        DATE_PART('DAY', T.PICKUP_DATETIME) AS VARCHAR(2)
    ) AS "day"
FROM NYC.TRIP T
WHERE 
    DATE_PART('YEAR', T.PICKUP_DATETIME) = 2012
    AND
    DATE_PART('MONTH', T.PICKUP_DATETIME) IN (
        SELECT 
            DISTINCT CAST(DATE_PART('MONTH', T.PICKUP_DATETIME) AS INTEGER) AS "MONTHS"
        FROM NYC.TRIP T
        WHERE 
            DATE_PART('YEAR', T.PICKUP_DATETIME) = 2012
        ORDER BY 1 DESC
        LIMIT 3
    )
GROUP BY 3, 4;
"""

base_dataset = pd.read_sql(query, con=get_connection())
base_dataset.month = base_dataset.month.apply(pd.to_numeric, errors='ignore')
base_dataset.day = base_dataset.day.apply(pd.to_numeric, errors='ignore')
base_dataset = base_dataset.sort_values(['month', 'day'], ascending=[True, True])
base_dataset.sample(3)
Out[17]:
tip key month day
62 1311.94 10/13 10 13
79 1253.89 9/23 9 23
81 1427.06 10/4 10 4

Agora com o auxílio do Matlibplot vamos montar mais uma vez, a visualização dos dados usando um grafico de barras, para ver a a série temporal das grojetas distribuídas ao longo dos dias.

In [18]:
fig, ax = plt.subplots()
fig.figsize=(24, 10)

ax.figure.set_size_inches(24, 10)
ax.set_ylabel("Gorjeta", fontsize=16)
ax.set_xlabel("Dia", fontsize=16)
ax.yaxis.set_tick_params(labelsize=14)
ax.xaxis.set_tick_params(labelsize=14)

w = .22
ax.bar(
    base_dataset.loc[base_dataset.month == 8].day.values - w,
    base_dataset.loc[base_dataset.month == 8].tip,
    color="#00f429",
    align = 'center',
    width=w
)
ax.bar(
    base_dataset.loc[base_dataset.month == 9].day.values,
    base_dataset.loc[base_dataset.month == 9].tip,
    color="b",
    align = 'center',
    width=w
)
ax.bar(
    base_dataset.loc[base_dataset.month == 10].day.values + w,
    base_dataset.loc[base_dataset.month == 10].tip,
    color="#ff0050",
    align = 'center',
    width=w
)

ax.legend(['Agosto','Setembro', 'Outubro'], loc='upper right', fontsize=12)
ax.xaxis.set_ticks(range(1, 33))
plt.show()

É possível ver acima que os registros estão muito equilibrados e que diariamente há mais que mil dólares gastos em gorjeta em táxis de Nova York.

Questões de Tempo

A partir de agora vamos ver nossos dados a partir de outro ponto muito importante. Vamos avaliar o tempo de corridas de táxis. Para isso, vamos limitar nossa população para uma pequena amostra, apenas os dias de final de semana (sábado e domingo).

Vamos realizar um consulta em nossa base que já vai trazer para nós o tempo de viagem em minutos, o horário de início e o horário de fim.

In [19]:
query = """
SELECT
    DATEDIFF('MINUTE', T.PICKUP_DATETIME::TIMESTAMP, T.DROPOFF_DATETIME::TIMESTAMP) AS "time_trip",
    T.PICKUP_DATETIME AS "pick_up",
    T.DROPOFF_DATETIME AS "drop_off"
FROM NYC.TRIP T
WHERE 
    EXTRACT(DOW FROM T.PICKUP_DATETIME::TIMESTAMP) IN (0, 6);
"""

base_dataset = pd.read_sql(query, con=get_connection())
base_dataset.sample(3)
Out[19]:
time_trip pick_up drop_off
769009 10 2011-09-11 23:14:38.785229 2011-09-11 23:24:59.290193
653021 8 2009-06-06 04:44:33.302525 2009-06-06 04:52:29.101798
394173 10 2010-11-28 10:02:55.125128 2010-11-28 10:12:10.629190

Selecionaremos os valores únicos para ver o quão complexo é fazer a distribuição desses valores em um histograma.

In [20]:
fig, ax = plt.subplots()
fig.figsize=(15, 10)
ax.figure.set_size_inches(15, 10)
ax.set_ylabel("Frequência de ocorrência", fontsize=13)
ax.set_xlabel("Tempo de viagem", fontsize=13)
ax.yaxis.set_tick_params(labelsize=13)
ax.xaxis.set_tick_params(labelsize=13)
plt.hist(base_dataset.time_trip, 50)
plt.show()

Com isso é poossível notar que as viagens que levam pouco tempo, são as mais comuns de acontecerem no final de semana. Vamos comprovar isso a partir da média:

In [21]:
round(base_dataset.time_trip.mean(), 2)
Out[21]:
8.75

Visualização em Mapa

Agora que já entendemos um ponco dos dados presentes em nossa base, vamos tentar plota-lós em um mapa. Pela quantidade gigantesca de registro, vamos reduzir nossas métricas, iremos utilizar apenas os dados referente ao ano de 2010 para exibir no maapa os pontos em que passageiros foram deixados.

Até o momento estivemos usando a biblioteca do matplotlib para exibir gráficos, mas vamos trocar para uma chamda Altair, que nos ajudará imensamente na renderização de dados em mapas com poucas linhas de código.

In [22]:
query = """
SELECT 
    T.DROPOFF_LONGITUDE AS  "longitude",
    T.DROPOFF_LATITUDE AS  "latitude"
FROM NYC.TRIP T
WHERE 
    DATE_PART('YEAR', T.PICKUP_DATETIME) = 2010;
"""

dropoff_dataset = pd.read_sql(query, con=get_connection())
dropoff_dataset.describe()
Out[22]:
longitude latitude
count 1000000.000000 1000000.000000
mean -72.848044 40.134998
std 9.050547 4.986515
min -79.191394 -0.015202
25% -73.990665 40.735366
50% -73.979534 40.754488
75% -73.963385 40.769523
max 0.008668 47.935812

É possivel notar a partir da função de describe, que existem alguns outliers, pois estão com os valores bem próximos de 0. Isso pode ser um problema quando a visualização for gerada. Não só valores

(Vamos usar o valor -73.7 a -74.3 para limitar a longitude e 40.1 a 40.9 para a latitude, pois geograficamente, representam pontos que delimitam a área de Nova York)

In [23]:
outlier_counter = dropoff_dataset.loc[
    (dropoff_dataset.longitude > -73.7) | (dropoff_dataset.latitude < 40.1 ) |
    (dropoff_dataset.longitude < -74.3) | (dropoff_dataset.latitude > 40.9 )
].longitude.count()
print("Quantidade de Outliers: {}".format(outlier_counter))
outlier_percentage = (outlier_counter / dropoff_dataset.latitude.count()) * 100
print("Porcentagem de Outilers no dataset: {}%".format(round(outlier_percentage, 2)))
Quantidade de Outliers: 18412
Porcentagem de Outilers no dataset: 1.84%

Como a quantidade de outliers é bem baixa, poderiamos fazer uma média das longitudes e latitudes e substituir nesses valores ou exclui-lós. Neste momento, vamos optar por exclui-lós pois existem muitos valores, e se substituíssemos todos pela mesma média, aconteceria de ficar uma grande marca no mapa.

In [24]:
dropoff_treated = dropoff_dataset.loc[ 
    (dropoff_dataset.longitude <= -73.7) & (dropoff_dataset.longitude >= -74.3 ) & 
    (dropoff_dataset.latitude >= 40.1 ) & (dropoff_dataset.latitude <= 40.9)
 ]
dropoff_treated.describe()
Out[24]:
longitude latitude
count 981588.000000 981588.000000
mean -73.974171 40.752194
std 0.033292 0.030432
min -74.299207 40.225140
25% -73.990842 40.736663
50% -73.979933 40.754975
75% -73.964936 40.769671
max -73.700588 40.899988
In [25]:
alt.data_transformers.disable_max_rows()
alt.renderers.enable('notebook')

# Vamos usar o Topojson da Cidade de New York, disponibilizados no Github.
# https://raw.githubusercontent.com/deldersveld/topojson/master/countries/us-states/NJ-34-new-jersey-counties.json
# https://raw.githubusercontent.com/codeforamerica/click_that_hood/master/public/data/new-york-city-boroughs.geojson
ny = alt.topo_feature('https://raw.githubusercontent.com/codeforamerica/click_that_hood/master/public/data/new-york-city-boroughs.geojson', 'feature')
In [26]:
background_two = alt.Chart(ny).mark_geoshape(
    fill='lightgray',
    strokeWidth=1,
    stroke='black',
    strokeOpacity=0
).properties(
    width=900,
    height=900
)

points = alt.Chart(dropoff_treated).mark_circle(
    size=7,
    opacity=0.05,
    stroke='transparent',
    color='red',
    strokeOpacity=1,
).encode(
    longitude='longitude:Q',
    latitude='latitude:Q'
)

(background_two + points).configure_view(stroke=None)
Out[26]:

Tomando como base a visualização, conseguimos entender o porquê das viagens terem uma média de duração muito curta. O ponto mais popular do gráfico, se encontra na região de Manhattan, uma área pequena para circulação e todas as suas sáidas dão diretamento para outtras cidades. Além de ser o ponto mais populos de NY.

Visualização ao Vivo

É muito comum no ambiente dos dias de hoje, haver a necessidade de visualizar os dados em tempo real de acordo com o que são atualizados. Uma das missões a se seguir é construir um gráfico que é atualizado em tempo real a partir de uma base de dados.

Para a construção da plotagem a seguir foi usado o primeiro dataset disponibilizado pelo Data Sprint, as viagns do ano de 2009. O dataset foi baixado e do mesmo estado que se encontrava foi feito upload para um bucket s3 da Amazon criado por mim mesmo para o exemplo. O bucket tem todas as configurações e políticas geradas no IAM para ser um bucket public com permissões para ser lido por qualquer um. As credênciais estão no código, mas vou mostrar no final da mensagem para poder ser utilizado livremente.

O uso do bucket foi escolhido para gerar um armazenamento de um dataset que pode ser atualizado a qualquer momento, recebendo mais linhas. E à partir desse bucket, podemos fazer um streaming para lê-lo e ficar atualizando constantemente a visualização. Pode se acontecer as vezes do dataset estourar o grafico pela quantidade de registros e pelo tempo que fica ativo, isso acontece devido ao grande dataset que possui 1 milhão de registros. Algumas técnicas de programação foram utilizadas para econômizar o processamento das linhas que já foram executadas e existe um espaçamento de tempo para não ocorrer estouro de memória.

Para construir essa animação do gráfico, foram usadas algumas novas tecnologias, como a bibioteca boto3 da AWS para conectar de forma simples com os buckets do S3(Simple Storage Service), threading para fazer o processamento em multi-thread (simplificando o processo de fazer 2 ações no mesmo tempo) e uma classe não vista anteriormente aqui, da biblioteca do matplotlib, a FuncAnimation().

Credênciais Bucket S3

(Obs: Caso a função não execute, é porque ela está estatica na visualização do notebook, para executa-lá, basta copiar o codigo dos próximos blocos e utiliza-lá em um notebook em execução).

No código a seguir, vamos criar uma função que executa a coneção com nosso bucket da Amazon S3 e faze um leve processamento par ver se a linha que ta sendo processada pelo stream já existe. Em seguida, a função com o auxílio do comando yield, devolve o valor para a função que a executou, e quando a própria função for executada novamente, ela vai continuar a partir do ultimo ponto que o yield parou. Para mais referências, aqui vai um artigo com uma ótima explicação sobre geradores(a função que utiliza yield): Link

O exemplo a frente foi utilizado uma lógica para trabalhar com os dados do Bucket, mas poderia ser adaptada para consultar constantemente dados de um banco, como o Redshift que foi construído anteriormente. A lógica se basearia em consulta nas quantidades de registros existentes e quando a contagem de valores mudar, uma nova consulta é executada para pegar as novas linhas inseridas mais recentementes e assim retornadas pelo yield da mesma forma.

In [27]:
import boto3
import json

def iter_lines_bucket(last_line = -1):
    ACCESS_ID = "AKIA23VGFWEPYHI4AKLI"
    ACCESS_KEY = "p2OvCfIm3qQxTAnZT7VBgdJAAT58/7Uk5V7L3wGv"
    
    url = 'https://stream-lauro.s3.us-east-2.amazonaws.com/dataset.json'
    bucket = 'stream-lauro'
    key = 'dataset.json'
    
    s3 = boto3.resource(
        's3',
         aws_access_key_id=ACCESS_ID,
         aws_secret_access_key= ACCESS_KEY
    )
    
    obj = s3.Object(bucket, key)
    for index, line in enumerate(obj.get()['Body']._raw_stream):
        if index <= last_line:
            continue;
        response = (None, None,)
        if line:
            try:
                chunk_dict = json.loads(line)
                response = (index, chunk_dict,)
            except Exception as ex: 
                print(ex)
        if index > 1000:
            break
        yield response

A seguir, existe uma classe contendo a execução da atualização ao vivo do gráfico em um gráfico do Matplotilib. Basicamente vamos construir aqui 2 funções de atualizações, que vão atualizar as linhas plotadas e uma função para a thread que vai ser executada em seguindo plano para atualizar os valores que estão sendo usadas no gráfico. A função da thread(thread_f) é quem chama um loop infinito para ficar consultando o bucket pela função que foi citada acima, iter_lines_bucket(). Toda vez que a função da thread for executada, ela vai adicionar os valores recebidos pelo streaming do bucket ao dataset que está sendo exibído. a função show() é apenas para plotar na tela.

Caso a função comece a travar durante a execução é devido ao uso grande de memória. E pelo mesmo motivo, vamos restringir o uso de registros, vamos disponibilizar apenas os 1000 primeiros registros.

In [28]:
%matplotlib notebook
from matplotlib import pyplot as plt
from matplotlib.animation import FuncAnimation
from random import randrange
import numpy as np
from threading import Thread
import time

class LiveGraph:
    def __init__(self):
        self.x_data, self.y_data, self.z_data = np.asarray([]), np.asarray([]), np.asarray([])
        self.figure = plt.figure()
        self.line, = plt.plot(self.x_data, self.y_data)
        self.line_two, = plt.plot(self.x_data, self.z_data, color="#ff0000")
        self.animation = FuncAnimation(self.figure, self.update, interval=1000)
        self.animation_z = FuncAnimation(self.figure, self.update_z, interval=1000)
        self.th = Thread(target=self.thread_f, daemon=True)
        self.th.start()

    def update(self, frame):
        self.line.set_data(self.x_data, self.y_data)
        self.figure.gca().relim()
        self.figure.gca().autoscale_view()
        return self.line,

    def update_z(self, frame):
        self.line_two.set_data(self.x_data, self.z_data)
        self.figure.gca().relim()
        self.figure.gca().autoscale_view()
        return self.line_two,

    def show(self):
        plt.show()

    def thread_f(self):
        last_line = 0
        x = 0
        while True: 
            for line, value in iter_lines_bucket(last_line):
                if line and value:
                    self.x_data = np.append(self.x_data, np.asarray([
                        x   
                    ]))
                    x+= 1
                    self.y_data = np.append(self.y_data, np.asarray([
                        value['trip_distance']    
                    ]))

                    self.z_data = np.append(self.z_data, np.asarray([
                        value['total_amount']    
                    ]))
                    last_line = line
                    time.sleep(.8)  

g = LiveGraph()
g.show()

O gráfico acima, duas variações. A linha vermelha representa o preço total da corrida e em azul é a distância percorrida. Com essa forma de visualização, conseguimos notar a proporção dos preços pelas distâncias viajadas ao longo da quantidade de registros, representado pela reta no eixo X.

Conclusão

Com isso finalizamos nossa análise que passa pelos principais dados das viagens de táxi em Nova York. Tento feito toda a exploração desses dados, podemos notar algumas peculiaridades e insights a partir de alguns gráficos. Valores que antes estavam ocultos em mares de registros.

O próximo passo a ser dado em estudos como esses, poderia ser a análise integrada com dados de fontes externos como clima, dias festivos, eventos acontecidos e assim perceber mais sentido nas variações de nossos dados ao longo dos dias.